package thread;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: wx
 * Date: 2024-01-24
 * Time: 12:42
 * *锁策略
 *
 *  *加锁过程中,处理冲突的过程中,涉及到的一些不同的处理方式
 *  *此处的所策略,并非是java所独有的
 *  *需要理解其中的相关概念
 *
 *  *第一组: 乐观锁 和 悲观锁  (这是两种不同的锁的实现方式)
 *  *       乐观锁,在加锁之前,预估当前出现锁冲突的概念不大,在进行加锁的时候就不会做太多的工作
 *  *             加锁过程中做的事情比较少,加锁的速度可能就会更快,但是更容易引起一些其他问题(譬如可能会消耗更多的 cpu 资源)
 *  *
 *  *       悲观锁,在加锁之前,预估从当前锁冲突出现的概率比较大,因此加锁的时候,就会做更多的工作
 *  *            做的事情更多,加锁的速度可能更慢,但是整个过程中不容易出现其他问题
 *  *
 *  *        之前疫情的时候,动不动就要封小区会出现有两种观点:
 *  *          1.乐观的看,觉得每次封小区的可能性不大,就不会做太多的准备
 *  *          2.悲观的看,就得被封的可能性挺大的的,就会做很多准备(囤一些物资),就是做更充分的准备,真的出现的时候,就可以更从容
 *
 *  *第二组: 轻量级锁 和 重量级锁
 *  *        轻量级锁,加锁的开销小,加锁的速度更快.   => 轻量级锁,一般就是乐观锁
 *  *        重量级锁,加锁的开销更大,加锁的速度更慢. =>重量级锁,一般也就是悲观锁
 *  *        轻量重量,加锁之后,对结果的评价
 *  *       悲观乐观,加锁之前,对未发生的事情进行的评估
 *  * 整体来说,两种锁描述的同一个事情
 *
 *  *第三组: 自旋锁 和 挂起等待锁
 *  *       自旋锁,就是轻量级锁的一种典型实现
 *  *             进行加锁的时候,搭配一个while 循环.如果加锁成功,自然循环结束            这个反复快速执行的过程,就称为 "自旋"
 *  *             如果加锁不成功,不是阻塞放弃 cpu,而是进行下一次循环,再次尝试获取到锁       一旦其他线程释放了锁,它就能立即拿到锁
 *  *                            同时,这样的锁,也是乐观锁.
 *  *                         使用自旋的前提,就是预期锁冲突概率不大,其他线程一旦释放了锁,就能第一时间拿到      加锁消耗时间段,但是需要消耗cpu
 *  *                         万一当前加锁的线程特别多,自旋意义就不大了,白白浪费 CPU 了.
 *  *                         也是乐观锁.预估了锁竞争不激烈才能使用
 *  *       挂起等待锁,就是重量级锁的一种典型实现   (进行挂起等待的时候,就需要内核介入了,这一块要完成的就多了.真正获取到锁花的时间也就多了)
 *  *           是一种悲观锁.  适用于锁冲突激烈的情况
 *  *            加锁时间不像自旋锁name快,线程一旦进入阻塞,就需要重新参与系统的调度
 *  *            那个时间能够调度上CPU就是未知数了.他的好处就是这个阻塞的过程中把 CPU 资源让出来,可以趁机做点别的事情
 *  *            java中的 synchronized 算是那种情况?\
 *  *            synchronized 具有自适应能力
 *  *            在某些情况下 是 乐观锁/轻量级锁/自旋锁
 *  *            在某些情况下 是 悲观锁/重量级锁/挂起等待锁
 *  *            内部会自动的评估当前锁冲突的激烈程度
 *  *            如果当前锁冲突的激烈程度不大,就处于 乐观锁/轻量级锁/自旋锁
 *  *            如果当前锁冲突的激励程度很大,就处于 悲观锁/重量级锁/挂起等待锁
 *  *
 *  *第四组:普通互斥锁 和 读写锁
 *  *       普通互斥锁类似于 synchronized 操作涉及到 加锁 和 解锁
 *  *       读写锁 把加锁分成了两种情况: 1)加读锁 2)加写锁
 *  *         读锁和读锁之间,不会出现锁冲突(不会阻塞)
 *  *         读锁和写锁之间,会出现锁冲突(会阻塞)
 *  *         写锁和写锁之间,会出现锁冲突(会阻塞)
 *  *   为什么要引入读写锁?
 *  *    如果两个线程读,本身就是线程安全的,不需要进行互斥
 *  *    如果使用 synchronized 这种方式加锁,两个线程读,也会产生互斥,产生阻塞(对性能有一定的损失)
 *  *    完全不加锁也不行,也不行,就怕一个线程读一个线程写,可能会读到写了一半的数据
 *  *    读写锁,就可以很好地解决上述的问题
 *  *
 *  *第五组:公平锁 和 非公平锁
 *  *     定义的 "公平" ,遵循先来后到,才叫公平.
 *  *     站在系统原生的锁的角度,就属于是"非公平锁"
 *  *     系统线程的调度本身就是无序的,随机的
 *  *     上一个线程释放锁之后,接下里唤醒那个线程不好说 (java 中的 synchronized 也是非公平的锁)
 *  *
 *  *     要想实现公平锁,就需要引入额外的数据结构(引入队列,记录每个线程先后顺序),才能实现公平锁
 *  *                 使用公平锁,天然就可以避免线程饿死的问题
 *  *
 *  *第六组:可重入锁 和 不可重入锁
 *  *    一个线程对一把锁,连续加锁两次,不会死锁,就是 可重入锁.会死锁,就是不可重入锁
 *  *    synchronized 是可重入锁.系统自带的锁,是不可重入的锁
 *  *    可重入锁需要记录持有锁的线程是谁,加锁的次数的计数
 *  *
 *  *
 *  *
 *  * 基于某个特定的锁
 *  *  对于synchronized来说:
 *  *  1乐观锁/悲观锁自适应 2.轻量级锁/重量级锁自适应  3.自旋锁/挂起等待锁自适应 4.不是读写锁 5.非公平锁 6.可重入锁
 *  *  对于系统原生的锁(Linux 提供的 mutex 这个锁):
 *  *  1.悲观锁 2.重量级锁 3.挂起等待锁 4.不是写入锁 5.非公平锁 6.不可重入锁
 *  */

/*

synchronized 内部的工作原理
 优化的非常好,大部分情况下使用 synchronized不会有啥问题
  自适应,是什么意思?
  当线程执行到synchronized的时候,如果这个对象当处于未加锁状态,就会经历以下过程
  1.偏向锁阶段
   核心思想,'懒汉模式',能不加锁,就不加锁.能晚加锁,就晚加锁.
   所谓的偏向锁,并非是真的加锁,而只是做了一个非常轻量标记

   搞暧昧,就是偏向锁,只是做一个标记,没有互斥
   一旦有其他线程,来和我竞争这个锁,就在另一个线程之前,先把锁获取到. 从偏向锁就会升级到轻量锁(真加锁了,就会有互斥了)
   如果没有竞争,整个过程就把加锁这样的操作完全省略了

  2.偏向锁阶段

  此处是通过自旋锁的方式来实现的
  优势: 另外的线程把锁释放了,就第一时间拿到了锁
  劣势: 比较消耗CPU
  与此同时,synchronized 内部也会进行统计.当前这个锁对象上,有多少个线程在参与竞争
  这里当发现参与竞争的线程比较多了,就会进一步升级到重量级锁

  对于自旋锁来说,如果竞争者很多.大量的线程都在自旋,整体CPU消耗就很大了

  3.重量级锁阶段
  此时拿不到锁的线程就不会继续自旋了,而是进入"阻塞等待",就会让出 CPU 了(不会使CPU占用率太高)

  锁消除
  也是synchronized 中内置的优化策略
  编译器优化的一种方式.编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动干掉锁
  这里的优化是比较保守的
  比如,就只有一个线程,在这一个线程里,加锁了. 或者说加锁代码中,没有涉及到"成员变量的修改",就只是一些 局部变量.....
  都不需要加锁

  其他,很多的"模棱两可",编译器也不知道这里是要加还是不加,都不会去消除
  锁消除,针对的是一眼看上去就完全不涉及线程安全问题的代码,能够把代码消除掉

  锁粗话
  会把多个细粒度的锁,合并成一个粗粒度的锁
  synchronized{} 大括号里边的代码越少,就认为锁粒度越细.
                 包含的代码越多,就认为锁的力度越粗.
  通常情况下,是更偏向于让锁的粒度细一些,更有利于多个线程并发执行
  但有的时候,却也希望锁的粒度粗点也挺好

*/

    /*
    CAS
    compare and swap (严格的来说,和java无关) 包括说 JVM 中关于 CAS 的api,都是放在 unsafe 包里(不推荐你用的东西)
    是一个特殊的CPU指令,完成的工作,就是"交换和比较"

    下边是一段伪代码 不能编译执行,只是用来表示逻辑
        boolean CAS(address,expectValue,swapValue) { address 地址 ,expectValue 寄存器中的值,swapValue 寄存器中的值
       if(&address == expectValue){ 比较address内存地址中的值,是否和 expected 寄存器中的值相同
           &address = swapValue;     如果相同,就把 swap 和 address 内存中的值,进行交换
           return true;              如果不相同,什么都不发生 返回FALSE
        }
       return false;
    }
    CAS本身就是 CPU 指令. 操做系统对指令进行了封装
     JVM 又对 操作系统 提供了的 api又封装了一层 有的CPU可能会不支持 CAS 的.
     java中 CAS放到了 unsafe 包里
                    这样的操作,涉及到一些底层内容,使用不当的话可能会带来一些分险
                    一般不建议使用 CAS
      java 标准库 对 CAS 又进行了进一步的封装,提供了一些工具类,让咱们直接使用
      最主要的一个工具,叫 "原子类"
    */

public class ThreadDemo33 {
    //不使用原生的int ,而是替换成原生的 AtomicInteger
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                //count++;
                count.getAndIncrement();
                //++count
                //count.incrementAndGet();
                //count += n
                //count.getAndAdd(n);
            }
        });

        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}
/*
对于ABA问题,解决方案
1)约定数据变化是单向的(只能增加或者只能减少),不能是双向的(又能增加又能减少)
2)对于本身就必须是双向变化的数据,可以给他引入一个版本号,版本号这个数字就是只能增加,不能减少




*/
